package interpreter

import (
	"fmt"
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
	"github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/declarative"
)

// AllResourceInterpreterCustomizationRules all InterpreterOperations
var AllResourceInterpreterCustomizationRules = []Rule{
	&retentionRule{},
	&replicaResourceRule{},
	&replicaRevisionRule{},
	&statusReflectionRule{},
	&statusAggregationRule{},
	&healthInterpretationRule{},
	&dependencyInterpretationRule{},
}

type retentionRule struct{}

func (r *retentionRule) Name() string {
	return string(configv1alpha1.InterpreterOperationRetain)
}

func (r *retentionRule) Document() string {
	return `This rule is used to retain runtime values to the desired specification.
The script should implement a function as follows:
function Retain(desiredObj, observedObj)
  desiredObj.spec.fieldFoo = observedObj.spec.fieldFoo
  return desiredObj
end`
}

func (r *retentionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.Retention != nil {
		return c.Spec.Customizations.Retention.LuaScript
	}
	return ""
}

func (r *retentionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.Retention = nil
		return
	}

	if c.Spec.Customizations.Retention == nil {
		c.Spec.Customizations.Retention = &configv1alpha1.LocalValueRetention{}
	}
	c.Spec.Customizations.Retention.LuaScript = script
}

func (r *retentionRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	desired, err := args.getDesiredObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	observed, err := args.getObservedObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	retained, enabled, err := interpreter.Retain(desired, observed)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("retained", retained)
}

type replicaResourceRule struct {
}

func (r *replicaResourceRule) Name() string {
	return string(configv1alpha1.InterpreterOperationInterpretReplica)
}

func (r *replicaResourceRule) Document() string {
	return `This rule is used to discover the resource's replica as well as resource requirements.
The script should implement a function as follows:
function GetReplicas(desiredObj)
  replica = desiredObj.spec.replicas
  nodeClaim = {}
  nodeClaim.hardNodeAffinity = {}
  nodeClaim.nodeSelector = {}
  nodeClaim.tolerations = {}
  return replica, nodeClaim
end`
}

func (r *replicaResourceRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.ReplicaResource != nil {
		return c.Spec.Customizations.ReplicaResource.LuaScript
	}
	return ""
}

func (r *replicaResourceRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.ReplicaResource = nil
		return
	}

	if c.Spec.Customizations.ReplicaResource == nil {
		c.Spec.Customizations.ReplicaResource = &configv1alpha1.ReplicaResourceRequirement{}
	}
	c.Spec.Customizations.ReplicaResource.LuaScript = script
}

func (r *replicaResourceRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	replica, requires, enabled, err := interpreter.GetReplicas(obj)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("replica", replica).add("requires", requires)
}

type replicaRevisionRule struct {
}

func (r *replicaRevisionRule) Name() string {
	return string(configv1alpha1.InterpreterOperationReviseReplica)
}

func (r *replicaRevisionRule) Document() string {
	return `This rule is used to revise replicas in the desired specification.
The script should implement a function as follows:
function ReviseReplica(desiredObj, desiredReplica)
  desiredObj.spec.replicas = desiredReplica
  return desiredObj
end`
}

func (r *replicaRevisionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.ReplicaRevision != nil {
		return c.Spec.Customizations.ReplicaRevision.LuaScript
	}
	return ""
}

func (r *replicaRevisionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.ReplicaRevision = nil
		return
	}

	if c.Spec.Customizations.ReplicaRevision == nil {
		c.Spec.Customizations.ReplicaRevision = &configv1alpha1.ReplicaRevision{}
	}
	c.Spec.Customizations.ReplicaRevision.LuaScript = script
}

func (r *replicaRevisionRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	revised, enabled, err := interpreter.ReviseReplica(obj, args.Replica)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("revised", revised)
}

type statusReflectionRule struct {
}

func (s *statusReflectionRule) Name() string {
	return string(configv1alpha1.InterpreterOperationInterpretStatus)
}

func (s *statusReflectionRule) Document() string {
	return `This rule is used to get the status from the observed specification.
The script should implement a function as follows:
function ReflectStatus(observedObj)
  status = {}
  status.readyReplicas = observedObj.status.observedObj
  return status
end`
}

func (s *statusReflectionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.StatusReflection != nil {
		return c.Spec.Customizations.StatusReflection.LuaScript
	}
	return ""
}

func (s *statusReflectionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.StatusReflection = nil
		return
	}

	if c.Spec.Customizations.StatusReflection == nil {
		c.Spec.Customizations.StatusReflection = &configv1alpha1.StatusReflection{}
	}
	c.Spec.Customizations.StatusReflection.LuaScript = script
}

func (s *statusReflectionRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	status, enabled, err := interpreter.ReflectStatus(obj)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("status", status)
}

type statusAggregationRule struct {
}

func (s *statusAggregationRule) Name() string {
	return string(configv1alpha1.InterpreterOperationAggregateStatus)
}

func (s *statusAggregationRule) Document() string {
	return `This rule is used to aggregate decentralized statuses to the desired specification.
The script should implement a function as follows:
function AggregateStatus(desiredObj, statusItems)
  for i = 1, #items do
    desiredObj.status.readyReplicas = desiredObj.status.readyReplicas + items[i].readyReplicas
  end
  return desiredObj
end`
}

func (s *statusAggregationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.StatusAggregation != nil {
		return c.Spec.Customizations.StatusAggregation.LuaScript
	}
	return ""
}

func (s *statusAggregationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.StatusAggregation = nil
		return
	}

	if c.Spec.Customizations.StatusAggregation == nil {
		c.Spec.Customizations.StatusAggregation = &configv1alpha1.StatusAggregation{}
	}
	c.Spec.Customizations.StatusAggregation.LuaScript = script
}

func (s *statusAggregationRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}

	status := args.Status
	if status == nil {
		status = []workv1alpha2.AggregatedStatusItem{}
	}
	aggregateStatus, enabled, err := interpreter.AggregateStatus(obj, status)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("aggregatedStatus", aggregateStatus)
}

type healthInterpretationRule struct {
}

func (h *healthInterpretationRule) Name() string {
	return string(configv1alpha1.InterpreterOperationInterpretHealth)
}

func (h *healthInterpretationRule) Document() string {
	return `This rule is used to assess the health state of a specific resource.
The script should implement a function as follows:
luaScript: >
function InterpretHealth(observedObj)
  if observedObj.status.readyReplicas == observedObj.spec.replicas then
    return true
  end
end`
}

func (h *healthInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.HealthInterpretation != nil {
		return c.Spec.Customizations.HealthInterpretation.LuaScript
	}
	return ""
}

func (h *healthInterpretationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.HealthInterpretation = nil
		return
	}

	if c.Spec.Customizations.HealthInterpretation == nil {
		c.Spec.Customizations.HealthInterpretation = &configv1alpha1.HealthInterpretation{}
	}
	c.Spec.Customizations.HealthInterpretation.LuaScript = script
}

func (h *healthInterpretationRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	healthy, enabled, err := interpreter.InterpretHealth(obj)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("healthy", healthy)
}

type dependencyInterpretationRule struct {
}

func (d *dependencyInterpretationRule) Name() string {
	return string(configv1alpha1.InterpreterOperationInterpretDependency)
}

func (d *dependencyInterpretationRule) Document() string {
	return ` This rule is used to interpret the dependencies of a specific resource.
The script should implement a function as follows:
function GetDependencies(desiredObj)
  dependencies = {}
  if desiredObj.spec.serviceAccountName ~= "" and desiredObj.spec.serviceAccountName ~= "default" then
    dependency = {}
    dependency.apiVersion = "v1"
    dependency.kind = "ServiceAccount"
    dependency.name = desiredObj.spec.serviceAccountName
    dependency.namespace = desiredObj.namespace
    dependencies[0] = {}
    dependencies[0] = dependency
  end
  return dependencies
end`
}

func (d *dependencyInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string {
	if c.Spec.Customizations.DependencyInterpretation != nil {
		return c.Spec.Customizations.DependencyInterpretation.LuaScript
	}
	return ""
}

func (d *dependencyInterpretationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) {
	if script == "" {
		c.Spec.Customizations.DependencyInterpretation = nil
		return
	}

	if c.Spec.Customizations.DependencyInterpretation == nil {
		c.Spec.Customizations.DependencyInterpretation = &configv1alpha1.DependencyInterpretation{}
	}
	c.Spec.Customizations.DependencyInterpretation.LuaScript = script
}

func (d *dependencyInterpretationRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
	obj, err := args.getObjectOrError()
	if err != nil {
		return newRuleResultWithError(err)
	}
	dependencies, enabled, err := interpreter.GetDependencies(obj)
	if err != nil {
		return newRuleResultWithError(err)
	}
	if !enabled {
		return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
	}
	return newRuleResult().add("dependencies", dependencies)
}

// Rule known how to get and set script for interpretation rule, and can execute the rule with given args.
type Rule interface {
	// Name returns the name of the rule.
	Name() string
	// Document explains detail of rule.
	Document() string
	// GetScript returns the script for the rule from customization. If not enabled, return empty
	GetScript(*configv1alpha1.ResourceInterpreterCustomization) string
	// SetScript set the script for the rule. If script is empty, disable the rule.
	SetScript(*configv1alpha1.ResourceInterpreterCustomization, string)
	// Run execute the rule with given args, and return the result.
	Run(*declarative.ConfigurableInterpreter, RuleArgs) *RuleResult
}

// Rules is a series of rules.
type Rules []Rule

// Names returns the names of containing rules.
func (r Rules) Names() []string {
	names := make([]string, len(r))
	for i, rr := range r {
		names[i] = rr.Name()
	}
	return names
}

// GetByOperation returns the matched rule by operation name, ignoring case. Return nil if none is matched.
func (r Rules) GetByOperation(operation string) Rule {
	if operation == "" {
		return nil
	}
	operation = strings.ToLower(operation)
	for _, rule := range r {
		ruleName := strings.ToLower(rule.Name())
		if ruleName == operation {
			return rule
		}
	}
	return nil
}

// Get returns the rule with the name. If not found, return nil.
func (r Rules) Get(name string) Rule {
	for _, rr := range r {
		if rr.Name() == name {
			return rr
		}
	}
	return nil
}

// RuleArgs rule execution args.
type RuleArgs struct {
	Desired  *unstructured.Unstructured
	Observed *unstructured.Unstructured
	Status   []workv1alpha2.AggregatedStatusItem
	Replica  int64
}

func (r RuleArgs) getDesiredObjectOrError() (*unstructured.Unstructured, error) {
	if r.Desired == nil {
		return nil, fmt.Errorf("desired, desired-file options are not set")
	}
	return r.Desired, nil
}

func (r RuleArgs) getObservedObjectOrError() (*unstructured.Unstructured, error) {
	if r.Observed == nil {
		return nil, fmt.Errorf("observed, observed-file options are not set")
	}
	return r.Observed, nil
}

func (r RuleArgs) getObjectOrError() (*unstructured.Unstructured, error) {
	if r.Desired == nil && r.Observed == nil {
		return nil, fmt.Errorf("desired-file, observed-file options are not set")
	}
	if r.Desired != nil && r.Observed != nil {
		return nil, fmt.Errorf("you can not specify both desired-file and observed-file options")
	}
	if r.Desired != nil {
		return r.Desired, nil
	}
	return r.Observed, nil
}

// NameValue name and value.
type NameValue struct {
	Name  string
	Value interface{}
}

// RuleResult rule execution result.
type RuleResult struct {
	Results []NameValue
	Err     error
}

func newRuleResult() *RuleResult {
	return &RuleResult{}
}

func newRuleResultWithError(err error) *RuleResult {
	return &RuleResult{
		Err: err,
	}
}

func (r *RuleResult) add(name string, value interface{}) *RuleResult {
	r.Results = append(r.Results, NameValue{Name: name, Value: value})
	return r
}
